iT邦幫忙

2023 iThome 鐵人賽

DAY 13
0
SideProject30

placeholder系列 第 13

30天打造線上多人桌遊網站-Day 13-Action Cable

  • 分享至 

  • xImage
  •  

0. 前情提要

昨天把專案透過 zeabur 部署上線。

https://ironman2023-hanabi.zeabur.app/

今天來替專案導入 Action Cable


1. Action Cable

https://guides.rubyonrails.org/action_cable_overview.html

Action Cable 是 Rails 對 WebSocket 的包裝,透過他來達成伺服器與瀏覽器的雙向即時通訊。

相關概念後面有時間可能會分出一篇來介紹,這裡先建立一個範例來驗證『雙向即時通訊』的可行性。

TBD Again...

--- updated at 04:00

2. Create Channel

rails g channel ChatRoom

定義如何識別不同的連線(websocket connection)

# app/channels/application_cable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
+   identified_by :uuid

    def connect
+     self.uuid = SecureRandom.urlsafe_base64
    end
  end
end

定義訂閱的頻道

# app/channels/chat_room_channel.rb

class ChatRoomChannel < ApplicationCable::Channel
  def subscribed
    # stream_from "some_channel"
+   stream_from 'public_channel'
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

3. use Stimulus to subscribe to channel

前端部分使用 Stimulus 在元件 connect 時訂閱頻道,並將 connected, disconnected, received 等方法注入進去。

// app/javascript/channels/chat_room_channel.js

import consumer from "channels/consumer";

export default function createRoom({ connected, disconnected, received }) {
  return consumer.subscriptions.create("ChatRoomChannel", {
    connected() {
      if (connected) {
        connected();
      } else {
        // Called when the subscription is ready for use on the server
        console.log("connected");
      }
    },

    disconnected() {
      // Called when the subscription has been terminated by the server
    },

    received(data) {
      if (received) {
        received(JSON.stringify(data));
      } else {
        // Called when there's incoming data on the websocket for this channel
        console.warn("received data");
      }
    },

    sendMessage(messageBody) {
      this.perform("foobar", messageBody);
    },
  });
}
// app/javascript/controllers/hello_controller.js

import { Controller } from "@hotwired/stimulus";
import chat_room_channel from "channels/chat_room_channel";
import {
  receivedFromUser,
  receivedFromWorld,
} from "controllers/receive_wrapper";

export default class extends Controller {
  static targets = ["title"];

  connect() {
    this.titleTarget.textContent = "hello controller loaded";
    this.channel = chat_room_channel({
      connected: this.connected.bind(this),
      received: this.received.bind(this),
    });
  }

  connected() {}

  received(data) {
    const { message, uuid, role, nickname } = JSON.parse(data);
    if (role === null || role === undefined) {
      console.warn("role is missing");
      return;
    }

    let insertElement;

    switch (role) {
      case "player":
        insertElement = receivedFromUser({ message, uuid, nickname });
        break;

      case "lobby":
        insertElement = receivedFromWorld(message);
        break;

      default:
        break;
    }

    this.element.insertAdjacentElement("beforeend", insertElement);
  }

  sendMessage(messageBody) {
    this.channel.sendMessage(messageBody);
  }

  ping_the_room() {
    this.sendMessage({
      nickname: "Paul",
      body: "This is a cool chat app.",
    });
  }
}
// app/javascript/controllers/receive_wrapper.js

const receivedFromWorld = (textContent) => {
  const html = document.createElement("p");
  html.textContent = "Lobby: " + textContent;
  return html;
};

const receivedFromUser = ({ message: textContent, uuid: user, nickname }) => {
  const html = document.createElement("p");

  html.textContent =
    (nickname ? `${nickname}(${user})` : user) + ": " + textContent;
  return html;
};

export { receivedFromUser, receivedFromWorld };

找個頁面把 Stimulus Controller 掛上去

// app/views/game_rooms/index.html.erb

+ <div class="min-w-full" data-controller="hello">
+   <p class="font-bold text-2xl" data-hello-target="title"></p>
+   <button type="button" data-action="hello#ping_the_room" class="rounded-lg py-3 px-5 bg-emerald-600 text-white">ping to the room</button>
+ </div>

4. receive / broadcast messages

定義 channel 可接受的 action

# app/channels/chat_room_channel.rb

class ChatRoomChannel < ApplicationCable::Channel
...
+ def foobar(data)
+   chat_to_public(data['body'], { nickname: data['nickname'] })
+ end
+ 
+ private
+ 
+ def broadcast(data, speaking: 'Lobby')
+   ActionCable.server.broadcast 'public_channel', { message: data, role: :lobby }
+  end

5. deploy

action cable 需要 redis 當作資料庫,在 zeabur 專案頁面 > 建立服務 > Marketplace > Redis

建立後到 Rails app 設定環境變數 REDIS_URL=${REDIS_URI} (或是改 cable.yml 也可以)

成功部署完後,可以看到頁面可以即時接收其他使用者傳遞出的訊息。

https://ithelp.ithome.com.tw/upload/images/20230929/20150987hRAjMx3Pm1.png

5. 結語

接下來就可以進入遊戲邏輯的實作部分了。 /images/emoticon/emoticon61.gif


上一篇
30天打造線上多人桌遊網站-Day 12-Deploy to zeabur
下一篇
30天打造線上多人桌遊網站-Day 14-遊戲進度保存
系列文
placeholder20
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言